// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: web_view_host_window.cpp
// Description: WebViewHostWindow Class
//      Author: Ziming Li
//     Created: 2020-10-16
// -------------------------------------------------
// Copyright (C) 2020 MingYang Software Technology.
// -------------------------------------------------

#include "./web_view_host_window.h"

#include <windowsx.h>
#include "blinkit/app/win_app.h"
#include "blinkit/blink/renderer/core/editing/Editor.h"
#include "blinkit/ui/animation/animation_frame.h"
#include "blinkit/ui/input_events.h"
#include "blinkit/ui/web_view_impl.h"
#include "blinkit/win/context_menu_controller.h"

namespace BlinKit {

static std::unordered_map<HWND, WebViewHostWindow *> g_hosts;

static constexpr UINT RESIZING_TIMER_ELAPSE = 100;
static constexpr DWORD RESIZING_COMMIT_ELAPSE = 500;
std::unordered_map<UINT_PTR, WebViewHostWindow *> WebViewHostWindow::m_resizingHosts;

WebViewHostWindow::WebViewHostWindow(const BkWebViewClient &client, HWND hWnd, LPCREATESTRUCT cs)
    : WebPageHostWindow(nullptr, hWnd)
    , m_webView(std::make_unique<WebViewImpl>(client, *this, PaintSession::DefaultBackgroundColor()))
{
    WebPageHost::AttachWebPage(m_webView.get());
    g_hosts.emplace(*this, this);
}

WebViewHostWindow::~WebViewHostWindow(void)
{
    g_hosts.erase(*this);
}

void WebViewHostWindow::AdjustUpdateWhileResizing(DWORD tick)
{
    if (tick - m_resizingTick < RESIZING_COMMIT_ELAPSE)
        return;

    m_resizingTick = tick;
    CommitAnimationImmediately();
}

bool WebViewHostWindow::CanBeClosed(void) const
{
    return 0 == (GetWindowLong(*this, GWL_STYLE) & WS_CHILD);
}

void WebViewHostWindow::ChangeTitle(const std::string &title)
{
    std::wstring ws = zed::multi_byte_to_wide_string(title, CP_UTF8);
    SetWindowTextW(*this, ws.c_str());
}

void WebViewHostWindow::Close(void)
{
    ASSERT(CanBeClosed());
    PostMessage(*this, WM_CLOSE, 0, 0);
}

WebViewHostWindow* WebViewHostWindow::CreateInstance(HWND hWnd, LPCREATESTRUCT cs)
{
    BkWebViewClient *client = reinterpret_cast<BkWebViewClient *>(cs->lpCreateParams);
    if (nullptr != client)
        return new WebViewHostWindow(*client, hWnd, cs);

    ASSERT(nullptr != client);
    return nullptr;
}

bool WebViewHostWindow::ForwardMessage(
    const BkWebViewClient &client,
    HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam,
    LRESULT &result)
{
    if (nullptr == client.ProcessMessage)
        return false;
    return client.ProcessMessage(hWnd, Msg, wParam, lParam, &result, client.UserData);
}

WebScreenInfo WebViewHostWindow::GetScreenInfo(void)
{
    WebScreenInfo ret;

    HMONITOR hMon = MonitorFromWindow(*this, MONITOR_DEFAULTTONULL);
    ASSERT(nullptr != hMon);
    if (nullptr != hMon)
    {
        MONITORINFO info = { 0 };
        info.cbSize = sizeof(info);
        GetMonitorInfo(hMon, &info);

        ret.rect.Assign(info.rcMonitor);
        ret.availableRect.Assign(info.rcWork);
    }
    else
    {
        ASSERT(nullptr != hMon);
    }

    return ret;
}

void WebViewHostWindow::KillResizingTimer(void)
{
    ASSERT(0 != m_resizingTimerId);
    KillTimer(nullptr, m_resizingTimerId);
    m_resizingHosts.erase(m_resizingTimerId);
    m_resizingTimerId = 0;
}

void WebViewHostWindow::OnChar(HWND hwnd, TCHAR ch, int)
{
    m_webView->ProcessKeyEvent(WebInputEvent::Char, ch, 0);
}

BOOL WebViewHostWindow::OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
    RECT rc;
    GetClientRect(hwnd, &rc);

    HDC dc = GetDC(hwnd);
    UINT dpi = GetDeviceCaps(dc, LOGPIXELSY);
    ASSERT(0 != dpi && GetDeviceCaps(dc, LOGPIXELSX) == dpi);
    InitializePaintSession(dc, rc.right, rc.bottom);
    ReleaseDC(hwnd, dc);

    m_webView->Initialize(ScaleFactorFromDPI(dpi));
    return TRUE;
}

void WebViewHostWindow::OnDPIChanged(HWND hwnd, UINT newDPI, const RECT *rc)
{
    ASSERT(false); // BKTODO: SetScaleFactor(ScaleFactorFromDPI(newDPI));
    SetWindowPos(hwnd, nullptr, rc->left, rc->top, rc->right - rc->left, rc->bottom - rc->top,
        SWP_NOZORDER | SWP_NOACTIVATE);
}

void WebViewHostWindow::OnIMEStartComposition(HWND hwnd)
{
    IntRect anchor, focus;
    if (m_webView->SelectionBounds(anchor, focus))
    {
        COMPOSITIONFORM cf = { 0 };
        cf.dwStyle = CFS_POINT | CFS_FORCE_POSITION;
        cf.ptCurrentPos = { focus.x(), focus.y() };

        HIMC hIMC = ImmGetContext(hwnd);
        ImmSetCompositionWindow(hIMC, &cf);
        ImmReleaseContext(hwnd, hIMC);
    }
}

void WebViewHostWindow::OnKey(HWND, UINT vk, BOOL fDown, int, UINT)
{
    WebInputEvent::Type type = fDown ? WebInputEvent::RawKeyDown : WebInputEvent::KeyUp;
    int modifiers = 0;
    if (GetKeyState(VK_SHIFT) < 0)
        modifiers |= WebInputEvent::ShiftKey;
    if (GetKeyState(VK_CONTROL) < 0)
        modifiers |= WebInputEvent::ControlKey;
    if (GetKeyState(VK_MENU) < 0)
        modifiers |= WebInputEvent::AltKey;
    m_webView->ProcessKeyEvent(type, vk, modifiers);
}

void WebViewHostWindow::OnKillFocus(HWND hwnd, HWND hwndNewFocus)
{
    HideTooltip();
    m_webView->SetIsActive(false);
    m_webView->SetFocus(false);
}

void WebViewHostWindow::OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)
{
    if (GetFocus() != hwnd)
        SetFocus(hwnd);
}

int WebViewHostWindow::OnMouseActivate(HWND hwnd, HWND hwndTopLevel, UINT codeHitTest, UINT msg)
{
    SetFocus(hwnd);
    return MA_ACTIVATE;
}

void WebViewHostWindow::OnMouseWheel(HWND hwnd, int xPos, int yPos, int zDelta, UINT fwKeys)
{
    POINT pt = { xPos, yPos };
    ScreenToClient(hwnd, &pt);

    MouseWheelEvent e(pt.x, pt.y, zDelta, 0 != (fwKeys & MK_SHIFT));
    m_webView->ProcessInput(e.Translate());
}

bool WebViewHostWindow::OnNCCreate(WPARAM wParam, LPARAM lParam, LRESULT &result)
{
    result = TRUE;
    return ForwardMessage(m_webView->GetClient(), *this, WM_NCCREATE, wParam, lParam, result);
}

bool WebViewHostWindow::OnNCDestroy(HWND hwnd, WPARAM wParam, LPARAM lParam, LRESULT &result)
{
    const BkWebViewClient client = m_webView->GetClient();

    delete this;

    result = 0;
    return ForwardMessage(client, hwnd, WM_NCDESTROY, wParam, lParam, result);
}

void WebViewHostWindow::OnSetFocus(HWND hwnd, HWND hwndOldFocus)
{
    m_webView->SetFocus(true);
    m_webView->SetIsActive(true);
}

void WebViewHostWindow::OnShowWindow(HWND hwnd, BOOL fShow, UINT status)
{
    WebPageHostWindow::OnShowWindow(hwnd, fShow, status);
    m_webView->SetVisibilityState(fShow ? PageVisibilityStateVisible : PageVisibilityStateHidden);
}

void WebViewHostWindow::OnSize(HWND, UINT state, int cx, int cy)
{
    if (SIZE_MINIMIZED == state)
        return;

    const IntSize size(cx, cy);
    if (m_webView->Size() == size)
        return;

    m_resizingTick = GetTickCount();
    WebPageHostWindow::Resize(size);
}

bool WebViewHostWindow::ProcessMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam, LRESULT &result)
{
    // MessageLogger _(Msg);

    bool processFinished = true;
    switch (Msg)
    {
        case WM_MOUSEMOVE:
        case WM_LBUTTONDOWN:
        case WM_LBUTTONUP:
            if (m_pagePopupSession.ForwardMouseMessage(hWnd, Msg, wParam, lParam, result))
                return true;
            if (WM_LBUTTONDOWN == Msg)
                HANDLE_WM_LBUTTONDOWN(hWnd, wParam, lParam, OnLButtonDown);
            processFinished = false;
            break;

        case WM_KEYDOWN:
            HANDLE_WM_KEYDOWN(hWnd, wParam, lParam, OnKey);
            break;
        case WM_KEYUP:
            HANDLE_WM_KEYUP(hWnd, wParam, lParam, OnKey);
            break;
        case WM_CHAR:
            HANDLE_WM_CHAR(hWnd, wParam, lParam, OnChar);
            break;
        case WM_IME_STARTCOMPOSITION:
            OnIMEStartComposition(hWnd);
            break;

        case WM_MOUSEWHEEL:
        case WM_MOUSEHWHEEL:
            HANDLE_WM_MOUSEWHEEL(hWnd, wParam, lParam, OnMouseWheel);
            break;

        case WM_SIZE:
            HANDLE_WM_SIZE(hWnd, wParam, lParam, OnSize);
            break;

        case WM_MOUSEACTIVATE:
            result = HANDLE_WM_MOUSEACTIVATE(hWnd, wParam, lParam, OnMouseActivate);
            break;
        case WM_SETFOCUS:
            HANDLE_WM_SETFOCUS(hWnd, wParam, lParam, OnSetFocus);
            break;
        case WM_KILLFOCUS:
            HANDLE_WM_KILLFOCUS(hWnd, wParam, lParam, OnKillFocus);
            break;

        case WM_ENTERSIZEMOVE:
            SetResizingTimer();
            processFinished = false;
            break;
        case WM_EXITSIZEMOVE:
            KillResizingTimer();
            processFinished = false;
            break;

        case WM_SHOWWINDOW:
            HANDLE_WM_SHOWWINDOW(hWnd, wParam, lParam, OnShowWindow);
            break;

        case WM_DPICHANGED:
            ASSERT(HIWORD(wParam) == LOWORD(lParam));
            OnDPIChanged(hWnd, HIWORD(wParam), reinterpret_cast<LPRECT>(lParam));
            break;
        case WM_CREATE:
            result = HANDLE_WM_CREATE(hWnd, wParam, lParam, OnCreate);
            break;

        case WM_NCCREATE:
            return OnNCCreate(wParam, lParam, result);
        case WM_NCDESTROY:
            return OnNCDestroy(hWnd, wParam, lParam, result);

        default:
            processFinished = false;
    }

    if (!processFinished)
        processFinished = WebPageHostWindow::ProcessMessage(hWnd, Msg, wParam, lParam, result);
    if (!ForwardMessage(m_webView->GetClient(), hWnd, Msg, wParam, lParam, result))
        return processFinished;
    return true;
}

void CALLBACK WebViewHostWindow::ResizingTimerProc(HWND, UINT, UINT_PTR timerId, DWORD tick)
{
    auto it = m_resizingHosts.find(timerId);
    ASSERT(m_resizingHosts.end() != it);
    it->second->AdjustUpdateWhileResizing(tick);
}

float WebViewHostWindow::ScaleFactorFromDPI(UINT dpi)
{
    static_assert(6 == DPI_LEVELS, "TODO: Process more DPI level(s)!");
    switch (dpi)
    {
        case 120:
            return 1.25;
        case 144:
            return 1.5;
        case 192:
            return 2.0;
        case 216:
            return 2.25;
        case 240:
            return 2.5;
    }
    ASSERT(USER_DEFAULT_SCREEN_DPI == dpi);
    return 1.0;
}

void WebViewHostWindow::SetResizingTimer(void)
{
    ASSERT(0 == m_resizingTimerId);
    m_resizingTimerId = SetTimer(nullptr, 0, RESIZING_TIMER_ELAPSE, ResizingTimerProc);
    m_resizingHosts[m_resizingTimerId] = this;
}

void WebViewHostWindow::ShowContextMenu(const WebContextMenuData &data)
{
    ContextMenuController &menuController = WinApp::Get().GetContextMenuController();

    POINT pt;
    GetCursorPos(&pt);

    HMENU hPopupMenu = menuController.RequireMenu(data.editFlags);

    SetForegroundWindow(*this);
    UINT returnCmd = TrackPopupMenu(hPopupMenu, TPM_RETURNCMD, pt.x, pt.y, 0, *this, nullptr);
    if (0 == returnCmd)
        return;

    if (CommandID::SelectAll == returnCmd)
    {
        GetWebPage()->GetFrame()->selection().selectAll();
        return;
    }

    ContextMenuController::EditorFunction pfn = nullptr;
    switch (returnCmd)
    {
        case CommandID::Undo:
            pfn = &Editor::undo;
            break;
        case CommandID::Cut:
            pfn = &Editor::cut;
            break;
        case CommandID::Copy:
            pfn = &Editor::copy;
            break;
        case CommandID::Paste:
            pfn = &Editor::pasteAsPlainText;
            break;
        case CommandID::Delete:
            pfn = &Editor::performDelete;
            break;
        default:
            ASSERT_NOT_REACHED();
            return;
    }
    menuController.RunEditorFunction(GetWebPage()->GetFrame()->editor(), pfn);
}

void WebViewHostWindow::StartPagePopupSession(PagePopupHostWindow *host)
{
    WebPageHostWindow::SetCapture();
    m_pagePopupSession.Start(*this, host);
}

void WebViewHostWindow::StopPagePopupSession(void)
{
    m_pagePopupSession.Stop();
    WebPageHostWindow::ReleaseCapture();
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#if 0 // BKTODO:
SkColor WebViewHost::DefaultBackgroundColor(void)
{
    COLORREF color = GetSysColor(COLOR_WINDOW);
    return SkColorSetARGB(0xff, GetRValue(color), GetGValue(color), GetBValue(color));
}
#endif

} // namespace BlinKit

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

using namespace BlinKit;

extern "C" {

BKEXPORT LRESULT CALLBACK BkDefWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
    do {
        WebViewHostWindow *host = zed::query_value(g_hosts, hWnd, nullptr);
        if (nullptr == host)
        {
            if (WM_NCCREATE != Msg)
                break;

            host = WebViewHostWindow::CreateInstance(hWnd, reinterpret_cast<LPCREATESTRUCT>(lParam));
            if (nullptr == host)
                return FALSE;
        }

        LRESULT r = 0;
        if (host->ProcessMessage(hWnd, Msg, wParam, lParam, r))
            return r;
    } while (false);

    WNDPROC defProc = IsWindowUnicode(hWnd) ? DefWindowProcW : DefWindowProcA;
    return defProc(hWnd, Msg, wParam, lParam);
}

BKEXPORT BkWebView BKAPI BkGetWebView(HWND hWnd)
{
    if (WebViewHostWindow *host = zed::query_value(g_hosts, hWnd, nullptr))
        return static_cast<WebViewImpl *>(host->GetWebPage());
    return nullptr;
}

} // extern "C"
